﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Represents a key signature.
    /// </summary>
    /// <remarks>
    /// <para>
    /// In musical notation, a key signature is a series of accidental symbols placed on the staff, 
    /// designating notes that are to be consistently played higher or lower than the equivalent 
    /// natural notes unless otherwise altered with an accidental. 
    /// Key signatures are generally written immediately after the clef at the beginning 
    /// of a line of musical notation, although they can appear in other parts of a score, notably after a double bar.
    /// </para>
    /// <para>
    /// Key signatures are generally used in a score to avoid the complication of having accidental symbols 
    /// on every instance of certain notes. Each major and minor key has an associated key signature that 
    /// sharpens or flattens the notes which are used in its scale. 
    /// However, it is not uncommon for a piece to be written with a key signature that does not match its key, 
    /// for example, in some Baroque pieces, or in transcriptions of traditional modal folk tunes.
    /// </para>
    /// <para>
    /// <i>Source: <a href="http://en.wikipedia.org/wiki/Key_signature">Wikipedia</a></i>
    /// </para>
    /// </remarks>
    /// <seealso cref="KeySignatureSign"/>
    /// <seealso cref="Accidental"/>
    /// <see cref="Note"/>
    public class KeySignature : EnvironmentModifier
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="KeySignature"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="measure">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure that this key signature logically belongs to.
        /// </para>
        /// </param>
        /// <param name="beat">
        /// <para>
        /// Type: <see cref="BeatElement"/>
        /// </para>
        /// <para>
        /// The beat that this symbol logically belongs to.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The <paramref name="measure"/> is <see langword="null"/>.
        /// </exception>
        internal KeySignature(XmlPackagePart packagePart, Staff owner, MeasureElement measure, BeatElement beat)
            : base(packagePart, owner, measure, beat)
        {
            key = new ElementCollection<KeySignatureSign>(this, keyElement);

            for (int i = 1; i <= 7; ++i)
                key.Add(new KeySignatureSign(PackagePart, this));
        }

        /// <summary>
        /// Creates a new <see cref="KeySignature"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal KeySignature(XmlPackagePart packagePart, Staff owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            keyElement = element[PackageVocabulary.KeyElementName, PackageVocabulary.PackageNamespaceUri];
            key = new ElementCollection<KeySignatureSign>(this, keyElement);
        }

        #endregion

        #region CoreSymbol Members

        /// <inheritdoc/>
        public override double Width
        {
            get 
            {
                double width = 0.0;

                for (int i = 0; i < 7; ++i)
                    width += key[i].Width;

                return width;
            }
        }

        /// <inheritdoc/>
        public override void ProcessEnvironmentChange(Environment environment)
        {
            OrderSigns(environment.CurrentClef);
        }

        #endregion

        #region EnvironmentModifier Members

        /// <inheritdoc/>
        protected override XmlElement CreateSymbolElementCore(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.KeySignatureElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            keyElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.KeyElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            element.AppendChild(keyElement);

            return element;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Positions the first key signature sign (order 0).
        /// </summary>
        /// <param name="note">Note of the positioned sign</param>
        /// <param name="clef">Current clef</param>
        /// <param name="max">Max sign staff position</param>
        /// <param name="min">Min sign staff position</param>
        /// <param name="down">Current direction</param>
        /// <returns>The staff position of the first sign</returns>
        private int PositionFirstSign(byte note, Clef clef, ref int max, ref int min, out bool down)
        {
            var tone = clef.GetToneFromPosition(clef.StaffPosition);

            tone.Note = note;
            int tonePos = clef.GetPositionFromTone(tone);
            if (tonePos + 7 < max)
            {
                tonePos += 7;
                ++tone.Octave;
            }
            else if (tonePos == max)
            {
                tonePos -= 7;
                --tone.Octave;
            }

            if (tonePos < max - 3)
            {
                down = false;
                min = tonePos - 1;
            }
            else
            {
                down = true;
                max = tonePos + 1;
            }

            return tonePos;
        }

        /// <summary>
        /// Positions signs with order 1 to 7.
        /// </summary>
        /// <param name="order">Array of bytes defining the ordering of the key signature signs</param>
        /// <param name="clef">Current clef</param>
        /// <param name="max">Max sing staff position</param>
        /// <param name="min">Min sign staff position</param>
        /// <param name="down">Current direction</param>
        /// <param name="lastTonePos">Staff position of the last positioned sign</param>
        /// <param name="position">Current horizontal position</param>
        /// <param name="positionedType">Array of accidental types that are now positioned</param>
        private void PositionRemainingSigns(byte[] order, Clef clef, int max, int min, bool down, int lastTonePos, ref double position, AccidentalType[] positionedType)
        {
            var tone = clef.GetToneFromPosition(clef.StaffPosition);

            for (int i = 1; i < 7; ++i)
            {
                tone.Note = order[i];
                int newTonePos = clef.GetPositionFromTone(tone);

                if (newTonePos < min)
                {
                    newTonePos += 7;
                    ++tone.Octave;
                }
                else if (newTonePos > max)
                {
                    newTonePos -= 7;
                    --tone.Octave;
                }

                if (down)                               // The next sign should be placed bellow the previous one
                {
                    if (newTonePos > lastTonePos)           // If the actual sign position is above the previous one
                    {
                        if (newTonePos - 7 >= min)      // Try to place it one octave lower and switch direction, otherwise
                        {                               // leave the direction intact
                            newTonePos -= 7;
                            --tone.Octave;
                            down = false;
                        }
                    }
                    else
                        down = false;                   // Switch direction, next sign will be placed above this one
                }
                else                                    // The next sign should be placed above the previou one...(analogically)
                {
                    if (newTonePos < lastTonePos)
                    {
                        if (newTonePos + 7 <= max)
                        {
                            newTonePos += 7;
                            ++tone.Octave;
                            down = true;
                        }
                    }
                    else
                        down = true;
                }

                if (positionedType.Any(t => key[order[i] - 1].AccidentalType == t))
                {
                    key[order[i] - 1].Position = position;
                    key[order[i] - 1].StaffPosition = newTonePos;
                    position += key[order[i] - 1].Width;
                }

                lastTonePos = newTonePos;
            }
        }

        /// <summary>
        /// Layouts the key signature signs according to the given clef.
        /// </summary>
        /// <param name="clef">Clef valid for the key signature</param>
        private void OrderSigns(Clef clef)
        {
            double position = 0.0;
            int maxStaffPos = FindOwner<Staff>().Descriptor.Lines * 2 - 1;

            // First layout sharps
            if (key.Any(s => s.AccidentalType == AccidentalType.Sharp || s.AccidentalType == AccidentalType.DoubleSharp))
            {
                int max = maxStaffPos;
                int min = -1;
                bool down;
                int tonePos = PositionFirstSign(sharpsOrder[0], clef, ref max, ref min, out down);

                if (key[sharpsOrder[0] - 1].AccidentalType == AccidentalType.Sharp || key[sharpsOrder[0] - 1].AccidentalType == AccidentalType.DoubleSharp)
                {
                    key[sharpsOrder[0] - 1].Position = position;
                    key[sharpsOrder[0] - 1].StaffPosition = tonePos;
                    position += key[sharpsOrder[0] - 1].Width;
                }

                PositionRemainingSigns(sharpsOrder, clef, max, min, down, tonePos, ref position, new AccidentalType[] {AccidentalType.Sharp, AccidentalType.DoubleSharp});
            }

            // Then flats
            if (key.Any(s => s.AccidentalType == AccidentalType.Flat || s.AccidentalType == AccidentalType.DoubleFlat))
            {
                int max = maxStaffPos;
                int min = -1;
                bool down;
                int tonePos = PositionFirstSign(flatsOrder[0], clef, ref max, ref min, out down);

                if (key[flatsOrder[0] - 1].AccidentalType == AccidentalType.Flat || key[flatsOrder[0] - 1].AccidentalType == AccidentalType.DoubleFlat)
                {
                    key[flatsOrder[0] - 1].Position = position;
                    key[flatsOrder[0] - 1].StaffPosition = tonePos;
                    position += key[flatsOrder[0] - 1].Width;
                }

                PositionRemainingSigns(flatsOrder, clef, max, min, down, tonePos, ref position, new AccidentalType[] { AccidentalType.Flat, AccidentalType.DoubleFlat });
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Copies the key defined by a given key signature to this key signature.
        /// </summary>
        /// <param name="source">
        /// <para>
        /// Type: <see cref="KeySignature"/>
        /// </para>
        /// <para>
        /// The key signature to be copied.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="source"/> is <see langword="null"/>.
        /// </exception>
        public void CopyKey(KeySignature source)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            for (byte i = 1; i < 7; ++i)
                key[i].AccidentalType = source[i].AccidentalType;

            var currentEnvironment = FindOwner<Staff>().GetEnvironmentFor(this);
            OrderSigns(currentEnvironment.CurrentClef);
            RaiseEnvironmentChanged();
        }

        /// <summary>
        /// Sets the accidental for a given note.
        /// </summary>
        /// <param name="note">
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The note for which the accidental is being set.
        /// A value from a range 1 to 7 (1 = C, 2 = D, ..., 7 = B [H]).
        /// </para>
        /// </param>
        /// <param name="accidental">
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The new accidental for the <paramref name="note"/>.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The <paramref name="note"/> is not in a range [1, 7].
        /// </exception>
        public void SetAccidental(byte note, AccidentalType accidental)
        {
            if (note < 1 || note > 7)
                throw new ArgumentOutOfRangeException();

            key[note - 1].AccidentalType = accidental;
            
            var currentEnvironment = FindOwner<Staff>().GetEnvironmentFor(this);
            OrderSigns(currentEnvironment.CurrentClef);
            RaiseEnvironmentChanged();
        }

        /// <summary>
        /// Sets the key represented by this key signature.
        /// </summary>
        /// <param name="newKey">
        /// <para>
        /// Type: <see cref="Array"/> of <see cref="CWNotation.AccidentalType"/>
        /// </para>
        /// <para>
        /// The new key definition (an array of exactly 7 accidental types, one for each note from C to B).
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="newKey"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="newKey"/> does not have exactly 7 elements.
        /// </exception>
        public void SetKey(AccidentalType[] newKey)
        {
            if (newKey == null)
                throw new ArgumentNullException("newKey");
            if (newKey.Length != 7)
                throw new ArgumentException("Invalid key definition (wrong number of elements)!", "newKey");

            for (byte i = 0; i < 7; ++i)
                key[i].AccidentalType = newKey[i];

            var currentEnvironment = FindOwner<Staff>().GetEnvironmentFor(this);
            OrderSigns(currentEnvironment.CurrentClef);
            RaiseEnvironmentChanged();
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the key signature sign for a given note.
        /// </summary>
        /// <param name="note">
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The note for which the key signature sign is requested.
        /// The value has to be from a range 1 to 7 and corresponds to 1 of the 7 basic tones (1 = C, 2 = D, ..., 7 = B (H)).
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="KeySignatureSign"/>
        /// </para>
        /// <para>
        /// The sign that expresses the key signature value for the given note.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The <paramref name="value"/> is not from a range 1 to 7.
        /// </exception>
        public KeySignatureSign this[byte note]
        {
            get 
            {
                if (note < 1 || note > 7)
                    throw new ArgumentOutOfRangeException("note");

                return key[note - 1]; 
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Defines the order of flats in the key signature.
        /// </summary>
        private static readonly byte[] flatsOrder = new byte[7] { ToneElement.B, ToneElement.E, ToneElement.A, ToneElement.D, ToneElement.G, ToneElement.C, ToneElement.F };
        /// <summary>
        /// Holds the key.
        /// </summary>
        private ElementCollection<KeySignatureSign> key;
        /// <summary>
        /// Holds the XML element that holds the key.
        /// </summary>
        private XmlElement keyElement;
        /// <summary>
        /// Defines the order of sharps in the key signature.
        /// </summary>
        private static readonly byte[] sharpsOrder = new byte[7] { ToneElement.F, ToneElement.C, ToneElement.G, ToneElement.D, ToneElement.A, ToneElement.E, ToneElement.B };

        #endregion
    }
}
